ECSとCodePipelineのローリングアップデート構成をCDKで実装してみた
こんにちは、つくぼし(tsukuboshi0755)です!
以前以下のブログで、CDK(TypeScript)でECSとECRのコンテナ構成を実装しました。
今回はこの構成に対して、CodeCommit及びCodeBuildを含むCodePipelineを追加し、ECSのローリングアップデート構成を実現するCDKコードを作ってみたいと思います!
前提条件
今回は以下の通り、CDKv2を使ってコードを書いていきます。
$ cdk version 2.83.1 (build 006b542)
またDockerクライアントとしては、Rancher Desktopを使用します。
$ rdctl version rdctl client version: 1.1.0, targeting server version: v1
全体構成
今回はVPC+ALB+ECS(Fargate)+ECRに加えて、CodeCommit+CodeBuild+CodePipelineのCI/CD構成を追加で作成し、nginxコンテナをパブリック公開します。
リポジトリ
コード全体については、以下のリポジトリに格納していますのでご参照ください。
tsukuboshi/cdk-microservices-rollingupdate-template
コード解説
CDKコードの中核となるlib/cdk-microservices-rollingupdate-template-stack.ts
について説明します。
なお以前のECSとECRのコンテナ構成をCDKで実装してみた | DevelopersIOで説明した箇所は省略します。
また今回ローリングアップデートで必要なビルド仕様ファイル及びイメージ定義ファイルについて触れますが、該当ファイルの詳細は以下のブログをご参照ください。
ECSのデプロイ設定
// Create ALB and ECS Fargate Service const service = new ecs_patterns.ApplicationLoadBalancedFargateService( this, "FargateService", { loadBalancerName: `${resourceName}-lb`, publicLoadBalancer: true, cluster: cluster, serviceName: `${resourceName}-service`, cpu: 256, desiredCount: 2, memoryLimitMiB: 512, assignPublicIp: true, taskSubnets: { subnetType: ec2.SubnetType.PUBLIC }, taskImageOptions: { family: `${resourceName}-taskdef`, containerName: `${resourceName}-container`, image: ecs.ContainerImage.fromEcrRepository(ecrRepository, "latest"), logDriver: new ecs.AwsLogDriver({ streamPrefix: `container`, logGroup: logGroup, }), }, deploymentController: { type: ecs.DeploymentControllerType.ECS, }, } );
deployController
のtypeにDeploymentControllerType.ECS
を設定し、ECS自身がデプロイを制御するように設定します。
この設定により、ECSタスクがローリングアップデートでデプロイされるようになります。
CodeCommitリポジトリの作成
// Create CodeCommit Repository const codeCommitRepository = new codecommit.Repository( this, "CodeCommitRepo", { repositoryName: `${resourceName}-codecommit-repo`, } );
CodePipelineのソースステージに指定するCodeCommitリポジトリを作成します。
なお今回は記述していませんが、以下のようにcode
にディレクトリパスを指定する事で、CodeCommitリポジトリが作成された後に、特定のディレクトリに含まれる初期コードをすぐにプッシュできます。
{ repositoryName: `${resourceName}-codecommit-repo`, code: codecommit.Code.fromDirectory( path.join(__dirname, "..", "app"), "main" ), }
CodeBuildプロジェクトの作成
// Create CloudWatch Log Group const buildLogGroup = new logs.LogGroup(this, "BuildLogGroup", { logGroupName: `/aws/codebuild/${resourceName}`, removalPolicy: RemovalPolicy.DESTROY, }); // Create CodeBuild Project const codeBuildProject = new codebuild.Project(this, "CodeBuildProject", { projectName: `${resourceName}-codebuild-project`, source: codebuild.Source.codeCommit({ repository: codeCommitRepository, }), environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_5_0, computeType: codebuild.ComputeType.SMALL, privileged: true, environmentVariables: { AWS_ACCOUNT_ID: { value: accountId, }, REPOSITORY_URI: { value: ecrRepository.repositoryUri, }, CONTAINER_BUILD_PATH: { value: ".", }, CONTAINER_NAME: { value: service.taskDefinition.defaultContainer?.containerName, }, }, }, logging: { cloudWatch: { logGroup: buildLogGroup, }, }, buildSpec: codebuild.BuildSpec.fromObject({ version: "0.2", phases: { pre_build: { commands: [ "echo Logging in to Amazon ECR...", "aws --version", `aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com`, "COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)", "IMAGE_TAG=${COMMIT_HASH:=latest}", ], }, build: { commands: [ "echo Build started on `date`", "echo Building the Docker image...", "docker build -t $REPOSITORY_URI:latest $CONTAINER_BUILD_PATH", "docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG", ], }, post_build: { commands: [ "echo Build completed on `date`", "echo Pushing the Docker images...", "docker push $REPOSITORY_URI:$IMAGE_TAG", "docker push $REPOSITORY_URI:latest", "echo Writing image definitions file...", 'printf \'[{"name":"%s","imageUri":"%s"}]\' $CONTAINER_NAME $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json', ], }, }, artifacts: { files: ["imagedefinitions.json"], }, }), });
CodePipelineのビルドステージに指定するCodeBuildプロジェクトを作成します。
今回はbuildSpec
にBuildSpec.fromObject
を設定し、ビルド仕様をコマンド形式で書く形にしています。
この形にする事で、ビルド仕様をCDKコード内に記述でき、CDKの機能を利用しやすくなります。
参考:CodeBuildのbuildspecはCDKのBuildSpec.fromObjectで作成するようにした - mazyu36の日記
なおコマンド形式ではなく、ソースに存在するビルド仕様ファイル(buildspec.yml)を読み込む形にしたい場合は、fromSourceFilename
を用いる事で実現できます。
その場合はBuildSpec
の箇所を以下の内容に書き換えた上で、CodeCommitリポジトリにビルド仕様ファイルをプッシュしてください。
buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec.yml')
CodeBuildサービスロールへのECRアクセス権付与
// Create ECR Access Policy const ecrAccessPolicy = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "ecr:BatchCheckLayerAvailability", "ecr:CompleteLayerUpload", "ecr:GetAuthorizationToken", "ecr:InitiateLayerUpload", "ecr:PutImage", "ecr:UploadLayerPart", ], resources: ["*"], }); // Add ECR Access Policy to CodeBuild Project codeBuildProject.addToRolePolicy(ecrAccessPolicy);
先程のProject
ではCodeBuildサービスロールも自動的に作成されますが、デフォルトではECRへのPush権限がついていません。
そのため必要なECR権限を、CodeBuildサービスロールに付与します。
参考:CodeBuild のDocker サンプル - AWS CodeBuild
S3アーティファクトバケットの作成
// Create Artifact Bucket for CodePipeline const artifactBucket = new s3.Bucket(this, "ArtifactBucket", { bucketName: `${resourceName}-artifact-bucket-${accountId}`, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, encryption: s3.BucketEncryption.S3_MANAGED, removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, });
CodePipelineで使用するS3アーティファクトバケットを作成します。
今回のS3バケットは検証用のため、autoDeleteObjects
をtrueで設定し、バケットにオブジェクトが存在する場合でも削除できるようにしています。
CodePipelineの作成
// Create CodePipeline const pipeline = new codepipeline.Pipeline(this, "CodePipeline", { artifactBucket: artifactBucket, pipelineName: `${resourceName}-pipeline`, }); // Add Source Stage to Pipeline const sourceOutput = new codepipeline.Artifact(`${resourceName}-source`); const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ actionName: "Source", repository: codeCommitRepository, output: sourceOutput, branch: "main", trigger: codepipeline_actions.CodeCommitTrigger.EVENTS, }); pipeline.addStage({ stageName: "Source", actions: [sourceAction], }); // Add Build Stage to Pipeline const buildOutput = new codepipeline.Artifact(`${resourceName}-build`); const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: "Build", project: codeBuildProject, input: sourceOutput, outputs: [buildOutput], }); pipeline.addStage({ stageName: "Build", actions: [buildAction], }); // Add Deploy Stage to Pipeline const deployAction = new codepipeline_actions.EcsDeployAction({ actionName: "Deploy", service: service.service, imageFile: buildOutput.atPath("imagedefinitions.json"), }); pipeline.addStage({ stageName: "Deploy", actions: [deployAction], });
CodePipelineを作成し、ソースステージ/ビルドステージ/デプロイステージを各々設定します。
CodeCommitSourceAction
のtriggerにCodeCommitTrigger.EVENTS
を設定する事で、CodePipeline用のEventBridgeルールが合わせて作成され、CodeCommitリポジトリにプッシュされると自動的にパイプラインが起動するようになります。
またEcsDeployAction
のimageFileには、ビルドステージで作成したイメージ定義ファイル(imagedefinitions.json)を指定します。
動作確認
CDKコードのデプロイ、及びECSタスクの起動確認については、ECSとECRのコンテナ構成をCDKで実装してみた | DevelopersIOをご参照ください。
ここではCodePipelineによるローリングアップデートの動作確認を実施します。
CDKコードをデプロイした後、パイプラインが以下の通り作成されている事を確認します。
続いてパイプラインを稼働させるため、コンソール上で作成されたCodeCommitリポジトリをクリックし、"ファイルのアップロード"をクリックします。
"Choose File"をクリックし、例としてGitHubリポジトリのapp/Dockerfile
を選択してください。
また今回は検証のため、"作成者名"及び"Eメールアドレス"は何でも構いません。
項目を入力し終えたら、"変更のコミット"をクリックします。
コミット完了後、正常に設定されていればパイプラインが稼働します。
数分後、Deployステージまで成功していればローリングアップデートの動作確認は完了です!
最後に
今回はCodeCommit及びCodeBuildを含むCodePipelineを追加し、ECSのローリングアップデート構成を実現するCDKコードを作ってみました。
ぜひCDKでECSとCodePipelineのローリングアップデート構成を実装する際に、参考にしてみてください。
なおローリングアップデート構成と対をなす、ブルー/グリーンデプロイ構成については、以下のブログで解説していますので、必要に応じてご参照ください。
以上、つくぼし(tsukuboshi0755)でした!